PEP: 475
Title: Retry system calls failing with EINTR
Version: $Revision$
Last-Modified: $Date$
Author: Charles-François Natali cf.natali@gmail.com, Victor Stinner victor.stinner@gmail.com
BDFL-Delegate: Antoine Pitrou solipsis@pitrou.net
Status: Final
Type: Standards Track
Content-Type: text/x-rst
Created: 29-July-2014
Python-Version: 3.5
Resolution: https://mail.python.org/pipermail/python-dev/2015-February/138018.html
摘要
标准库中提供的系统调用函数在捕获到 EINTR
错误时能够自动重试,以减轻程序编码的负担。
所谓系统调用,我们指的是标准C函数库提供的操作 I/O 或者其他系统资源的函数。
基本原理
中断系统调用
在 POSIX 系统中,信号是很常见的,系统调用编码的时候必须准备捕获它们。一些常见的例子:
最常见的是
SIGINT
信号,当按下 CTRL+C 时发送该信号。 Python 在默认情况下捕获该信号后会抛出一个KeyboardInterrupt
异常。当使用到子进程时,子进程退出时会发送
SIGCHLD
信号。改变终端窗口大小时会向在该终端中运行的应用程序发送
SIGWINCH
信号。通过 CTRL+z 或者
SIGWINCH
命令将应用程序放到后台执行时发送SIGCONT
信号。
编写一个安全的 C 信号处理器是困难的:因为并不是所有的函数都是 “异步信号安全的” (例如,printf()
和 malloc()
函数就不是异步信号安全的),同时要处理好信号中断后的重入也是很麻烦的。然后幸运的是,当进程在执行系统调用的过程中被信号中断而失败的时会返回 EINTR
错误以便于程序处理,这样就不必强制要求函数是信号安全的。
这是一种依赖于系统的行为:在某些系统中,设置了 SA_RESTART
标识后,一些系统调用在捕获到 EINTR
错误后会自动重试。尽管如此,当在 Python 中调用 signal.signal()
函数设置信号处理器后会清除 SA_RESTART
标识:这样一来,在 Python 中所有的系统调用都可能会被信号中断而导致失败。
因为接收到一个信号并不是发生异常,所以健壮的 POSIX 编程要求必须能够处理 EINTR
错误(在大多情况下,也就意味着将一个希望操作成功的系统调用函数包装在一个循环中)。如果没有 python 提供的原生支持,应用程序编码就会更繁琐(由于python提供了支持,所以我们不需要专门去处理 EINTR
错误,这样可以使 python 代码更简洁。)。
Python 3.4 中的情况
在 Python 3.4 中,捕获 InterruptedError
异常(专门用于包装 EINTR
错误的异常类型)的代码被复制到各处系统调用处。但实际上也只有一小部分模块捕获了该异常,要修复这个问题让所有的 Python 模块都处理该异常需要花费好几年的时间。下面是一段捕获 InterruptedError
异常并自动重试 file.read()
的代码示例:
1 | while True: |
Python 标准库中已经实现内部捕获 InterruptedError
异常的模块列表:
asyncio
asyncore
io
,_pyio
multiprocessing
selectors
socket
socketserver
subprocess
其他编程语言比如 Perl, Java 和 Go 中系统调用时捕获 EINTR
错误并自动重试已经在语言底层实现了,所以不会影响到库和应用程序。
Use Case 1: Don’t Bother With Signals
在大多数场景中,你可能并不想被信号中断,也不希望捕获到 InterruptedError
异常。举个例子,你真的希望为了一个 “Hello World” 的示例代码而写上一段如此复杂的代码吗?
1 | while True: |
InterruptedError
异常可能发生在任何一个你不期望发生的地方。举个例子,os.close()
和 FileIO.close()
调用都可能抛出 InterruptedError
。参见这篇文章:close() and EINTR。
下面有关 Python issues related to EINTR
的章节将会给出一些由 EINTR
导致 bug 的例子。
在这种使用场景下,我们希望 Python 能够自行隐藏 InterruptedError
异常并自动重试系统调用。
Use Case 2: Be notified of signals as soon as possible
有时候,我们希望能够立即捕获并处理某些信号。举个例子,你可能希望当在键盘按下快捷键 CTRL+c
时就立即退出程序。
另外,对于某些信号我们应用程序既不感兴趣也不希望被其打断。这里有两种方式来处理我们感兴趣的信号:
设置一个自定义的信号处理器,这个信号处理器收到信号后抛出特定的异常类型,比如说为
SIGINT
信号抛出KeyboardInterrupt
类型的异常。结合 Python 的信号唤醒文件描述符机制使用
select()
一类的 I/O 复用函数:详见函数signal.set_wakeup_fd()
。
我们期望在这种情况下, Python 的信号处理器能够及时地接收处理信号,当信号处理器抛出异常时系统调用要么失败要么重试。
建议
PEP建议在底层实现捕获 EINTR 错误码并重试,也就是说,该功能应该被包装在标准库中(而不是在上层库和应用程序中)。
特别需要指出的是,在 Python 中当一个系统调用被 EINTR
中断时会调用相应的信号处理器(内部调用 PyErr_CheckSignals()函数)。如果该信号处理器抛出一个异常,那么系统调用失败并向上层传递异常。
否则,Python将自动重试系统调用。如果该系统调用设置了超时时间,超时时间将重新计算。
需要修改的函数
下面列举了一些需要修改以符合上述 PEP 的标准库函数:
open()
,os.open()
,io.open()
functions of the
faulthandler
moduleos
functions:os.fchdir()
os.fchmod()
os.fchown()
os.fdatasync()
os.fstat()
os.fstatvfs()
os.fsync()
os.ftruncate()
os.mkfifo()
os.mknod()
os.posix_fadvise()
os.posix_fallocate()
os.pread()
os.pwrite()
os.read()
os.readv()
os.sendfile()
os.wait3()
os.wait4()
os.wait()
os.waitid()
os.waitpid()
os.write()
os.writev()
- special cases:
os.close()
andos.dup2()
now ignoreEINTR
error,
the syscall is not retried
select.select()
,select.poll.poll()
,select.epoll.poll()
,select.kqueue.control()
,select.devpoll.poll()
socket.socket()
methods:accept()
connect()
(except for non-blocking sockets)recv()
recvfrom()
recvmsg()
send()
sendall()
sendmsg()
sendto()
signal.sigtimedwait()
,signal.sigwaitinfo()
time.sleep()
(注意: selector
模块目前虽然捕获到 InterruptedError
异常时会自动重试,但是它不会重新计算超时时间。)
os.close
, close()
以及 os.dup2()
方法例外: 它们会忽略掉 EINTR
错误而不会重试。原因比较复杂涉及到 Linux 底层,发生 EINTR 错误返回后相关的文件描述符可能真的已经被关闭了(注:所以就不用重试关闭)。参考下面的文章:
Returning EINTR from close()
(LKML) Re: [patch 7/7] uml: retry host close() on EINTR
close() and EINTR
对于非阻塞套接字(non-blocking sockets) socket.socket.connect()
方法被信号(失败并返回 EINTR
错误)中断后不会自动重试。因为连接操作已经异步在后台运行(注:后台已经异步向对方发出连接请求,只需等待对方应答,重试的话会导致重复连接),这时调用者需要自己负责等待套接字变得 “可读” (例如使用 select.select()
),然后调用 socket.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
检查连接是否成功(getsockopt()
返回 0
表示成功)。
InterruptedError 异常处理
由于系统调用被信号中断后将会自动重试,所以 InterruptedError
异常将不会被抛出,那么上面 Python3.4中的情况
章节所写的捕获 InterruptedError
的代码便可以移除,这样可简化标准库。
向后兼容
试想这样一类应用程序,当其系统调用被 InterruptedError
中断后会被挂起。PEP的作者没有考虑这类应用程序,因为这类程序引入了另外一些问题,比如竞态条件(系统调用执行前被信号中断将可能导致死锁)。此外,这样的代码也没有可移植性。
无论如何,这些应用程序都必须被修复以处理不同的信号,兼容不同的平台和 Python 版本。一个可行的方法是设置一个信号处理器抛出一个明确定义的异常或者使用信号唤醒文件描述符。
应用程序使用事件循环驱动时,推荐使用 signal.set_wakeup_fd()
来处理信号中断。 Python 底层的信号处理器将会把信号 number 写入唤醒文件描述符,事件循环被唤醒后便可以根据信号 number 处理信号。这样一来,事件循环便能不受约束地处理各种信号(比如,事件循环可以被任何线程唤醒而不仅仅局限于主线程)。
附录
唤醒文件描述符
从 Python 3.3 开始 signal.set_wakeup_fd()
会把信号 number 写入唤醒文件描述符,而之前是写入一个null字节(’\0’)。这样一来 python3.3 之后我们就可以通过唤醒文件描述符区分不同的信号。
Linux 下有一个叫 signalfd()
的系统调用函数可以提供关于信号的更多信息。比如,通过该函数可以获得发送该信号的 pid 和 uid 。但是很遗憾在 Python 中该函数不可用(详见 issue 12304
)
在 Unix 上, asyncio
模块使用唤醒文件描述符来唤醒其事件循环。
多线程化
C 语言中信号处理器可以被任何线程调用,但是在 Python 中只能被主线程调用。
Python 的 C API 提供了 PyErr_SetInterrupt()
函数来调用 SIGINT 的信号处理器就是为了中断Python的主线程。
Windows 中的信号
控制事件(Control events)
Windows 中使用 “control events”:
CTRL_BREAK_EVENT
: Break (SIGBREAK
)CTRL_CLOSE_EVENT
: Close eventCTRL_C_EVENT
: CTRL+C (SIGINT
)CTRL_LOGOFF_EVENT
: LogoffCTRL_SHUTDOWN_EVENT
: Shutdown
SetConsoleCtrlHandler() 函数
可以用来安装一个 控制事件 处理器。
通过 GenerateConsoleCtrlEvent() 函数
可以向一个进程发送CTRL_C_EVENT
和 CTRL_BREAK_EVENT
事件。该函数功能在 Python 中是通过 os.kill()
函数来提供。
信号
Windows 中支持的信号列表:
SIGABRT
SIGBREAK
(CTRL_BREAK_EVENT
): signal only available on WindowsSIGFPE
SIGILL
SIGINT
(CTRL_C_EVENT
)SIGSEGV
SIGTERM
SIGINT
在 Windows 中对于 SIGINT
信号,Python 默认的信号处理器会设置一个 Windows 事件对象: sigint_event
。
time.sleep()
函数是用 WaitForSingleObjectEx()
来实现的, time.sleep()
函数通过设置一个等待 sigint_event
对象的超时时间来达到效果。所以(在Windows)中 Sleep 是可以被信号中断的。
_winapi.WaitForMultipleObjects()
也是自动添加 sigint_event
到监视处理列表,因此也可以被信号中断。
当 fgets()
失败时 PyOS_StdioReadline()
也是使用 sigint_event
来检查是否按下 CTRL+c 或者 CTRL+z 。
Links
Misc
glibc manual: Primitives Interrupted by Signals
Bug #119097 for perl5: print returning EINTR in 5.14
Python issues related to EINTR
The main issue is: handle EINTR in the stdlib
Open issues:
Add a new signal.set_wakeup_socket() function
signal.set_wakeup_fd(fd): set the fd to non-blocking mode
Use a monotonic clock to compute timeouts
sys.stdout.write on OS X is not EINTR safe
platform.uname() not EINTR safe
asyncore does not handle EINTR in recv, send, connect, accept
socket.create_connection() doesn't handle EINTR properly
Closed issues:
Interrupted system calls are not retried
Solaris: EINTR exception in select/socket calls in telnetlib
subprocess: Popen.communicate() doesn't handle EINTR in some cases
multiprocessing.util._eintr_retry doen't recalculate timeouts
file readline, readlines & readall methods can lose data on EINTR
multiprocessing BaseManager serve_client() does not check EINTR on recv
selectors behaviour on EINTR undocumented
asyncio: limit EINTR occurrences with SA_RESTART
smtplib.py socket.create_connection() also doesn't handle EINTR properly
Faulty RESTART/EINTR handling in Parser/myreadline.c
test_httpservers intermittent failure, test_post and EINTR
os.spawnv(P_WAIT, ...) on Linux doesn't handle EINTR
asyncore fails when EINTR happens in pol
file.write and file.read don't handle EINTR
socket.readline() interface doesn't handle EINTR properly
subprocess is not EINTR-safe
SocketServer doesn't handle syscall interruption
subprocess deadlock when read() is interrupted
time.sleep(1): call PyErr_CheckSignals() if the sleep was interrupted
siginterrupt with flag=False is reset when signal received
need siginterrupt() on Linux - impossible to do timeouts
[Windows] Can not interrupt time.sleep()
Python issues related to signals
Open issues:
signal.default_int_handler should set signal number on the raised exception
expose signalfd(2) in the signal module
missing return in win32_kill?
Interrupts are lost during readline PyOS_InputHook processing
cannot catch KeyboardInterrupt when using curses getkey()
Deferred KeyboardInterrupt in interactive mode
Closed issues:
Implementation
The implementation is tracked in issue 23285
. It was committed on
February 07, 2015.
Copyright
This document has been placed in the public domain.
..
Local Variables:
mode: indented-text
indent-tabs-mode: nil
sentence-end-double-space: t
fill-column: 70
coding: utf-8